package sumoTween;

import java.util.ArrayList;
import java.util.Vector;

import sumoTween.easing.Cubic;
import sumoTween.easing.EasingIF;
import sumoTween.events.KeyEvent;
import sumoTween.events.TweenEvent;
import sumoTween.timer.TimerIF;
import sumoTween.timer.freeTimer;

public class Tweener 
{
	public static final int EASING_IN=0;
	public static final int EASING_OUT=1;
	public static final int EASING_INOUT=2;
	public static final int EASING_NONE=3;

	private boolean isKeyframed=true;
	private KeyFloat keyframes[];
	private int lastNotifiedKey=-1;
	EasingIF defaultEasing;

	TimerIF timer=null;
	
	Vector tweenEventListeners=null;
	
	
	public Tweener()
	{
		defaultEasing =(EasingIF)new Cubic();
		initKeyframes();
	}

	public Tweener(EasingIF easing)
	{
		this.defaultEasing =easing;
		initKeyframes();
	}
	
	private void initKeyframes()
	{
		if(timer==null)timer=freeTimer.timer;
		keyframes=new KeyFloat[1];
		keyframes[0]=new KeyFloat(this,0, 0, EASING_NONE);
		keyframes[0].setEasing(defaultEasing);
		if(tweenEventListeners==null)tweenEventListeners=new Vector();
	}
	
	public void addTweenEventListener(TweenEvent ev)
	{
		tweenEventListeners.add(ev);
	}
	

	protected void notifyKeysChanged()
	{
		for(int i=0;i<tweenEventListeners.size();i++)
		{
			TweenEvent ev=(TweenEvent)tweenEventListeners.elementAt(i);
			ev.keysChanged();
		}
	}

	/**
	 * adds a new key to the tweener. 
	 *
	 * @param  time		relative time of the new key
	 * @param  value 	float value of the new key
	 */
	public KeyFloat add(float timeDiff, float value)
	{
		return set(timer.getTime()+timeDiff, value, EASING_NONE,null,null);
	}

	public KeyFloat add(float timeDiff, float value, int dir, EasingIF ease, KeyEvent keyEvent) 
	{
		return set(timer.getTime()+timeDiff, value, dir,ease,keyEvent);
	}

	public KeyFloat add(float timeDiff, float value, int dir, KeyEvent event)
	{
		return set(timer.getTime()+timeDiff, value, dir,null,event);
	}

	public KeyFloat add(float timeDiff, float value, int dir, EasingIF easing)
	{
		return set(timer.getTime()+timeDiff, value, dir,easing,null);
	}

	public KeyFloat add(float timeDiff, float value, int dir)
	{
		return set(timer.getTime()+timeDiff, value, dir,null,null);
	}

	public KeyFloat set(float time, float value)
	{
		return set(time, value, EASING_NONE,null,null);
	}

	public KeyFloat add(float timeDiff, float value, KeyEvent keyEvent) {
		return set(timeDiff, value, EASING_NONE,null,keyEvent);
	}

	public KeyFloat set(float time, float value, int dir)
	{
		return set(time, value, dir,null,null);
	}

	public KeyFloat set(float time, float value, int dir, EasingIF ease)
	{
		return set(time, value, dir,ease,null);
	}

	/**
	 * adds a new key to the tweener. 
	 *
	 * @param  time		absolute time of the new key
	 * @param  value 	float value of the new key
	 * @param  dir 		direction (in / out / inout /)
	 * @param  event 	an event to execute, when the key is reached.
	 */
	public KeyFloat set(float time, float value, int dir, EasingIF ease, KeyEvent event)
	{
		KeyFloat ret=null;
		if(!isKeyframed)
		{
			keyframes[0].setValue(value);
			if(event!=null)keyframes[0].addKeyEventListener(event);
			ret=keyframes[0];
			notifyKeysChanged();
		}
		else
		{
			boolean alldone=false;
			for(int i=0;i<keyframes.length;i++)
			{
				if(keyframes[i].getTime()==time)
				{
					keyframes[i].setValue(value);
					keyframes[i].setEasingDirection(dir);
					keyframes[i].addKeyEventListener(event);
					normalize();
					alldone=true;
					notifyKeysChanged();
					ret=keyframes[i];
				}
			}

			if(!alldone)
			{
				if(time>keyframes[keyframes.length-1].getTime())
				{
					keyframes=(KeyFloat[])resizeArray(keyframes,1);
					keyframes[keyframes.length-1]=new KeyFloat(this,time,value,dir);
					keyframes[keyframes.length-1].addKeyEventListener(event);
					ret=keyframes[keyframes.length-1];
					
				}
				else
				{
					int keystart=getKeyIndex(time);
					keyframes=(KeyFloat[])resizeArray(keyframes,1);

					for(int i=keyframes.length-1;i>keystart+1;i--)
					{
						keyframes[i]=keyframes[i-1];
					}
					keyframes[keystart+1]=new KeyFloat(this,time,value,dir);
					keyframes[keystart+1].addKeyEventListener(event);
					ret=keyframes[keystart+1];
				}
				notifyKeysChanged();
				normalize();
			}
		}
		
		if(ease!=null)
			ret.setEasing(ease);
		else
			ret.setEasing(defaultEasing);
		
		return ret;
	}

	
	private KeyFloat getMaxValueKey()
	{
		KeyFloat max=new KeyFloat(this,0, -9999999f, EASING_NONE);
		for(int i=0;i<keyframes.length;i++){
			if(keyframes[i]!=null)
				if(keyframes[i].getValue()>max.getValue())max=keyframes[i];
		}
		return max;
	}
	
	private KeyFloat getMinValueKey()
	{
		KeyFloat min=new KeyFloat(this,0, 9999999f, EASING_NONE);
		for(int i=0;i<keyframes.length;i++)
		{
			if(keyframes[i]!=null)
				if(keyframes[i].getValue()<min.getValue())min=keyframes[i];
		}
		return min;
	}
	
	public float allValue=0.0f;
	public KeyFloat min;
	private String name="";
	
	public void normalize()
	{
		min=getMinValueKey();
		KeyFloat max=getMaxValueKey();

		allValue=Math.abs(max.getValue() - min.getValue());

//		System.out.println(min.getValue()+" min !!");
//		System.out.println(max.getValue()+" max !!");
//		System.out.println("diff: "+allValue);

		for(int i=0;i<keyframes.length;i++)
		{
			if(keyframes[i]!=null)
			{
				float idiff=Math.abs(keyframes[i].getValue() - min.getValue());
				keyframes[i].setInternalValue(idiff/allValue);
				
			}
		}
	}
	


	

	/**
	 * @param time
	 * @return
	 */
	public int getKeyIndex(float time)
	{
		int which=0;
		for(int i=0;i<keyframes.length;i++)
		{
			if(keyframes[i].getTime()<=time)
			{
				which=i; 
			}
		}
		return which;
	}

	public float getValue(float time)
	{
		return getValue(time, true);
	}
	
	public float getValue(float time, boolean notify)
	{

		float ret=0.0f;
		if(!isKeyframed)
		{
			if(notify && 0!=lastNotifiedKey)
			{
				lastNotifiedKey=0;
				keyframes[0].notifyKeyEventListeners();
			}
			ret=keyframes[0].getValue();
		}
		else 
		{
			if(min==null)normalize();
			ret=getInternalValue(time, notify)*allValue+min.getValue();
		}
		
		if(Float.isNaN(ret))
		{
			System.out.println("NAN!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
			ret=0.0f;
		}
		
		
		return ret;
	}
	
	
	public float getInternalValue(float time, boolean notify)
	{

		if(keyframes.length==1)return keyframes[0].getValue();
		
		// reached last key
		if(time>=keyframes[keyframes.length-1].getTime())
		{
			if(notify && keyframes[keyframes.length-1].hasKeyEventListeners())
			{
				lastNotifiedKey=keyframes.length-1;
				keyframes[lastNotifiedKey].notifyKeyEventListeners();
			}

			return keyframes[keyframes.length-1].getInternalValue();
		}
		
		int which=getKeyIndex(time);
		int next=which+1;


		float ttime=time-keyframes[which].getTime();
		float ratio=0f;

		if(notify && lastNotifiedKey!=which && keyframes[which].hasKeyEventListeners())
		{
			lastNotifiedKey=which;
			keyframes[which].notifyKeyEventListeners();	
		}
		

		
		EasingIF theEasing=keyframes[which].getEasing();
		if(theEasing==null)return 0.0f;
		// calculate the ratio between the two values... 
		if(keyframes[which].getEasingDirection()==EASING_IN)
			ratio=theEasing.easeIn( ttime, 0, 1, keyframes[next].getTime()-keyframes[which].getTime());
		else
			if(keyframes[which].getEasingDirection()==EASING_OUT)
				ratio=theEasing.easeOut( ttime, 0, 1, keyframes[next].getTime()-keyframes[which].getTime());
			else
				if(keyframes[which].getEasingDirection()==EASING_INOUT)
					ratio=theEasing.easeInOut(ttime, 0, 1, keyframes[next].getTime()-keyframes[which].getTime());
				else
					if(keyframes[which].getEasingDirection()==EASING_NONE)
						ratio=theEasing.easeNone( ttime, 0, 1, keyframes[next].getTime()-keyframes[which].getTime());


		float r=1.0f;

		// calculate internal value using ratio
		float di=Math.abs(keyframes[next].getInternalValue()- keyframes[which].getInternalValue());

//		if(keyframes[next].getInternalValue()==keyframes[which].getInternalValue())
//		{
//			lastNotifiedKey=which;
//			keyframes[which].notifyKeyEventListeners();	
//			return  keyframes[which].getInternalValue();
//		}


		if(keyframes[next].getValue()>keyframes[which].getValue())
		{
			r=keyframes[which].getInternalValue()+di*ratio;
		}
		else
		{
			r=keyframes[which].getInternalValue()-di*ratio;
		}


		

		// return internal value
		
		if(Float.isNaN(r))
			{
			
			r= 0.0f;
			System.out.println("NAN!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
			}
		return r;

	}


	private int getLastkeyIndex() {
		return keyframes.length-1;
	}

	private KeyFloat[] resizeArray(KeyFloat[] storedObjects, int growthFactor) 
	{
		if(storedObjects == null){ 
			throw new NullPointerException();
		}
		int currentCapacity = storedObjects.length;
		int newCapacity = currentCapacity + growthFactor;//((currentCapacity * growthFactor) / 100);
/*		if (newCapacity == currentCapacity ) {
			newCapacity++;
		}
		*/
		KeyFloat[] newStore = new KeyFloat[ newCapacity ];
		int l=storedObjects.length;
		if(newCapacity<storedObjects.length)l=newCapacity;
		System.arraycopy( storedObjects, 0, newStore, 0, l );
		return newStore;

		
	}


	public void printKeys()
	{
		System.out.println("------------------------------------");
		
		String easing="";
		if(getEasing()!=null)
			easing+=getEasing().getClass().getSimpleName();
		
		System.out.println("tween: "+getName()+" keyframed:"+isKeyframed+" easing:"+easing);

		System.out.println(
				"time \t"+
				"intervalue \t"+
				"value \t"+
				"events \t"+
				"easing"
				);

		for(int i=0;i<keyframes.length;i++)
		{
			
			String eas="";
			if(keyframes[i].getEasing()!=null)
				keyframes[i].getEasing().getClass().getSimpleName();
			
			if(keyframes[i]==null)System.out.println("null!");
			else
				System.out.println(
					keyframes[i].getTime()+" \t "+
					keyframes[i].getInternalValue()+"\t\t"+
					keyframes[i].getValue()+"\t"+
					keyframes[i].hasKeyEventListeners()+"\t"+
					eas);
		}
	}



	public int getIntValue() {
		return (int)getValue(timer.getTime(),true);
	}
	
	public void clear(float f) {
		initKeyframes();
		keyframes[0].setValue(f);
		notifyKeysChanged();
	}
	
	public void clear() {
		float val=getLastKey().getValue();
		clear(val);
	}

	public float getTime(){
		return timer.getTime();
	}
	
	public boolean isAnimFinished(){
		return getTime() > getLastKey().getTime();
	}
	
	public void setEasing(EasingIF easing) {
		this.defaultEasing=easing;
	}

	public EasingIF getEasing() {
		return defaultEasing;
	} 

	public KeyFloat getLastKey() {
		return keyframes[keyframes.length-1];
	}
	
	public float getValue() {
		return getValue(timer.getTime());
	}

	public KeyFloat getKey(float time)	{
		return keyframes[getKeyIndex(time)];
	}

	public int getIntValue(float time) {
		return (int)getValue(time,true);
	}

	public int getIntValue(float time, boolean notify) {
		return (int)getValue(time,notify);
	}

	public void resetTimer() {
		timer.setTime(0f);
	}

	public void setTimer(TimerIF timer) {
		this.timer=timer;
	}

	public void set(float value) 
	{
		set(timer.getTime(),value);
	}

	// fix this...
	public float slopeDiff=0.25f;
	public double getAngle() 
	{
		float v1=getInternalValue(freeTimer.timer.getTime()+slopeDiff, false);
		float v2=getInternalValue(freeTimer.timer.getTime()-slopeDiff, false);

		double angle=180*Math.atan2((v2-v1),(-2*slopeDiff))/Math.PI;

		return angle;
	}
	

	public boolean isKeyframed()
	{
		return isKeyframed;
	}

	public void setKeyframed(boolean b)
	{
		isKeyframed=b;
	}
	
	public void setName(String name)
	{
		this.name=name;
	}
	
	public String getName() {
		return name;
	}

	public KeyFloat[] getKeys() {
		return keyframes;
	}

	public KeyFloat getKeyByIndex(int i) {
		return keyframes[i];
	}

	public float getNormalizedValue() {
		return getNormalizedValue(timer.getTime());
	}

	public float getNormalizedValue(float time) {
		return getInternalValue(time, false);
	}

	public KeyFloat add(KeyFloat k) {
		return set(k.getTime(),k.getValue(),k.getEasingDirection());
	}

	public void deleteKeys(ArrayList<KeyFloat> markedKeys) {
		System.out.println("before:"+keyframes.length);
		for (KeyFloat key : markedKeys) {
			deleteKeyAt(key.getTime());
		}
		System.out.println("after:"+keyframes.length);
	}

	public void deleteKeyAt(float time) 
	{
		
		for(int i=0;i<keyframes.length;i++)
		{
			if(keyframes[i].getTime()==time)
			{
				System.out.println("key found..");
				keyframes[i].setTime(999999999f); // haxx
			}
		}
		sort();
		resizeArray(keyframes, -1);
	}

	boolean enabled=true;
	
	public void setEnabled(boolean b) 
	{
		notifyKeysChanged();
		enabled=b;
	}
	
	public boolean isEnabled()
	{
		return enabled;
	}

	public void sort() 
	{
		int out, in;
		
		for(int i=0;i<keyframes.length;i++)
		{
			if(Float.isNaN(keyframes[i].getTime()))
			{
				keyframes[i].setTime(0f);
				System.out.println("found NaN!!!");
			}
		}
		
		
		for (out = keyframes.length - 1; out > 0; out--)
		{
			
			
			for (in = 0; in < out; in++) 
			{
				if(keyframes[in].getTime() > keyframes[out].getTime())      
				{
					KeyFloat temp=keyframes[in];
					keyframes[in]=keyframes[out];
					keyframes[out]=temp;
				}
			}
		}
	}
	
	String titlevalues[];
	
	
	
	

	public void setTitleValues(String values[])
	{
		titlevalues=values;
	}
	
	public String getTitleValue() {
		if(titlevalues==null)return "";

		int i=getIntValue();
		if(i<0 || i>=titlevalues.length)return " ---";
		return ": "+titlevalues[i];
		
		
	}

	public int getNumKeys() {
		return keyframes.length;
	}


	


}


